﻿\subsection{调试技术}

\verb{bochsdbg} 允许程序设计者以指令级别调试操作系统代码。
在设计主引导记录及引导加载器时，可通过使用 \verb{bochsdbg} 来排查汇编程序中的错误。
主引导记录及引导加载器的汇编程序由设计者自行编写，并且代码量较少，调试的负担不算很大。
而在操作系统内核开发中，将主要使用 C 语言，辅以相对较少的汇编完成开发工作。
若仍采用 \verb{bochsdbg} 进行汇编级调试，定位程序错误的难度陡然增大。
本小节主要介绍内核断言和远程调试两种常用的内核调试技术。

\subsubsection{日志}

基于前述字符串打印函数，本项目中位内核设定了四种日志级别，通过在合适位置插入不同级别的日志信息，可在内核运行过程中从宏观上判断内核此时的状态。
例如，下述代码中 \lstinline{kerror} 就用于打印错误日志。

\begin{minted}[breakautoindent=true, breaklines]{cpp}
#define printk(...) VideoTextModePrintFormatted(__VA_ARGS__)

#define kerror(...) { \
    int tmp = g_VideoTextModeColor; \
    g_VideoTextModeColor = VIDEO_TEXT_MODE_COLOR_ERR; \
    printk(__VA_ARGS__); \
    g_VideoTextModeColor = tmp; \
}
\end{minted}

\subsubsection{内核断言}

断言是一种实用的调试手段，通过在程序中通常不可能出错的位置插入断言，可缩小错误所在范围，断言在前述的日志打印功能基础上实现。
本项目中的断言宏函数 \verb{KERNEL_ASSERT} 将在断言条件出错时，打印出错的源代码文件路径、行号等信息，其声明如下。

\begin{minted}[breaklines, breakautoindent=true]{cpp}
void panic(LPCSTR lpszFileName, DWORD dwLine, LPCSTR lpszFunc, LPCSTR lpszCondition);
#define KERNEL_PANIC(CONDITION) panic(__FILE__, __LINE__, __FUNCTION__, CONDITION)
#define KERNEL_ASSERT(CONDITION, ...) \
    if (!(CONDITION)) { \
        kerror(__VA_ARGS__); \
        KERNEL_PANIC(#CONDITION); \
    }
\end{minted}

其中，\verb{panic} 函数实现如下，它使用了之前实现的格式化字符串打印函数，断言宏函数就是使用 \verb{panic} 打印出错信息的。
当断言出错时，表明内核进入了绝不应该出现的错误状态，此时由 \verb{panic} 打印出错信息，然后使 CPU 处于停机状态，避免继续运行造成更严重的错误。

\begin{minted}[breaklines, breakautoindent=true]{cpp}
void panic(LPCSTR lpszFileName, DWORD dwLine, LPCSTR lpszFunc, LPCSTR lpszCondition)
{
    __asm cli
    kerror("[PANIC] %s in %s\r\nfile:%s\r\n:%d", lpszCondition, lpszFunc, lpszFileName, dwLine);
    while (TRUE) __asm hlt
}
\end{minted}

\subsubsection{远程调试技术}

前述的日志、断言能够从宏观上缩小程序中的错误范围。
在大致确定程序错误位置后，还需要在 C 语言语句甚至汇编层面确认错误的精确位置，这时就需引入调试器。
内核调试与一般程序调试有所不同，内核程序以 0 特权级运行于虚拟机上，调试器需要经由虚拟机接管对内核程序的控制权。

调试器在程序运行时，将当前执行的指令替换为 \verb{int3} 指令。执行到该指令时将触发 CPU 异常，中断当前程序执行流，进而在程序指令处下断点以实现汇编级调试，有关中断和异常相关的内容将在下一节详细介绍。
此外，编译器在编译源程序时，还可通过增设调试选项在生成目标文件中引入调试信息。
调试信息按照固定格式以文本段的形式存放于最终的可执行文件中，如 DWARF\cite{dwarf-doc}（Debugging With Attributed Record Formats）就是 ELF 二进制格式目标文件常用的调试信息格式。
调试信息可以直接保存在目标文件中，也可以单独存放于某个文件中。
存储有调试信息的这类文件在调试器中通常称作 \textbf{符号文件（Symbol File）}。
通过识别符号文件中的调试信息，程序开发人员就可对程序进行源码级别的调试。

Bochs 自带的调试器支持类似 GNU/GDB 的指令级调试功能，对于 16 位实模式下的汇编级调试拥有非常好的支持（GNU/GDB 等调试器无法正确反汇编 16 位指令）。
并且作为专用于操作系统的指令级调试器，还可方便地查看 CPU 中诸多控制寄存器（如 \verb{GDTR}）、系统数据结构（如 GDT、IDT 等），这一点是 GNU/GDB 做不到的。
因此，Bochs 及 \verb{bochsdbg} 非常适合于主引导记录和引导加载器的调试。
但是对于以 C 语言编写的操作系统内核，若仍采用指令级调试的方法，程序错误定位将非常耗时。

QEMU 与 Bochs 类似，提供了对 x86 的模拟支持。
QEMU 原生支持内核开发人员使用 GNU/GDB 或 LLVM-LLDB 这样的工具对内核进行调试。
GNU/GDB 与 LLVM-LLDB 相比于 \verb{bochsdbg}，能够识别目标文件中的调试信息，进而支持源码级别的调试，也支持 32 位或 64 位的汇编级调试（不支持 16 位汇编）。

QEMU 以指定调试选项启动后，将在虚拟器上开放一个 TCP 端口（一般默认为 1234，可自行指定），调试器可通过连接到该 TCP 端口对虚拟机中的操作系统内核进行 \textbf{远程调试}，这种调试仅仅是指令级别的。欲实现源码级别的调试，需要在调试器中指定符号文件的路径。本项目中使用调试器脚本实现符号文件的自动加载。例如，LLVM-LLDB 可通过下述脚本装载符号文件，并连接虚拟机进行调试。

\begin{minted}[breaklines, breakautoindent=true]{text}
settings set target.x86-disassembly-flavor intel
target create --no-dependents --arch i386 ./build/SimpleOS.out
target modules load --file SimpleOS.out --slide 0
gdb-remote localhost:1234
breakpoint set --name SimpleMain
\end{minted}
